<?php
/**
 * Zend Framework
 * LICENSE
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 * @category Zend
 * @package Zend_Filter
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 * @version $Id: Openssl.php 24593 2012-01-05 20:35:02Z matthew $
 */
/**
 *
 * @see Zend_Filter_Encrypt_Interface
 */
require_once 'Zend/Filter/Encrypt/Interface.php';

/**
 * Encryption adapter for openssl
 * @category Zend
 * @package Zend_Filter
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 */
class Zend_Filter_Encrypt_Openssl implements Zend_Filter_Encrypt_Interface {

    /**
     * Definitions for encryption
     * array(
     * 'public' => public keys
     * 'private' => private keys
     * 'envelope' => resulting envelope keys
     * )
     */
    protected $_keys = array('public' => array(), 'private' => array(), 'envelope' => array());

    /**
     * Internal passphrase
     * @var string
     */
    protected $_passphrase;

    /**
     * Internal compression
     * @var array
     */
    protected $_compression;

    /**
     * Internal create package
     * @var boolean
     */
    protected $_package = false;

    /**
     * Class constructor
     * Available options
     * 'public' => public key
     * 'private' => private key
     * 'envelope' => envelope key
     * 'passphrase' => passphrase
     * 'compression' => compress value with this compression adapter
     * 'package' => pack envelope keys into encrypted string, simplifies decryption
     * @param string|array $options Options for this adapter
     */
    public function __construct ($options = array()) {
        if ( ! extension_loaded ('openssl')) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception ('This filter needs the openssl extension');
        }
        if ($options instanceof Zend_Config) {
            $options = $options -> toArray ();
        }
        if ( ! is_array ($options)) {
            $options = array('public' => $options);
        }
        if (array_key_exists ('passphrase', $options)) {
            $this -> setPassphrase ($options['passphrase']);
            unset ($options['passphrase']);
        }
        if (array_key_exists ('compression', $options)) {
            $this -> setCompression ($options['compression']);
            unset ($options['compress']);
        }
        if (array_key_exists ('package', $options)) {
            $this -> setPackage ($options['package']);
            unset ($options['package']);
        }
        $this -> _setKeys ($options);
    }

    /**
     * Sets the encryption keys
     * @param string|array $keys Key with type association
     * @return Zend_Filter_Encrypt_Openssl
     */
    protected function _setKeys ($keys) {
        if ( ! is_array ($keys)) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception ('Invalid options argument provided to filter');
        }
        foreach ($keys as $type => $key) {
            if (is_file ($key) and is_readable ($key)) {
                $file = fopen ($key, 'r');
                $cert = fread ($file, 8192);
                fclose ($file);
            } else {
                $cert = $key;
                $key = count ($this -> _keys[$type]);
            }
            switch ($type) {
                case 'public' :
                    $test = openssl_pkey_get_public ($cert);
                    if ($test === false) {
                        require_once 'Zend/Filter/Exception.php';
                        throw new Zend_Filter_Exception ("Public key '{$cert}' not valid");
                    }
                    openssl_free_key ($test);
                    $this -> _keys['public'][$key] = $cert;
                    break;
                case 'private' :
                    $test = openssl_pkey_get_private ($cert, $this -> _passphrase);
                    if ($test === false) {
                        require_once 'Zend/Filter/Exception.php';
                        throw new Zend_Filter_Exception ("Private key '{$cert}' not valid");
                    }
                    openssl_free_key ($test);
                    $this -> _keys['private'][$key] = $cert;
                    break;
                case 'envelope' :
                    $this -> _keys['envelope'][$key] = $cert;
                    break;
                default :
                    break;
            }
        }
        return $this;
    }

    /**
     * Returns all public keys
     * @return array
     */
    public function getPublicKey () {
        $key = $this -> _keys['public'];
        return $key;
    }

    /**
     * Sets public keys
     * @param string|array $key Public keys
     * @return Zend_Filter_Encrypt_Openssl
     */
    public function setPublicKey ($key) {
        if (is_array ($key)) {
            foreach ($key as $type => $option) {
                if ($type !== 'public') {
                    $key['public'] = $option;
                    unset ($key[$type]);
                }
            }
        } else {
            $key = array('public' => $key);
        }
        return $this -> _setKeys ($key);
    }

    /**
     * Returns all private keys
     * @return array
     */
    public function getPrivateKey () {
        $key = $this -> _keys['private'];
        return $key;
    }

    /**
     * Sets private keys
     * @param string $key Private key
     * @param string $passphrase
     * @return Zend_Filter_Encrypt_Openssl
     */
    public function setPrivateKey ($key, $passphrase = null) {
        if (is_array ($key)) {
            foreach ($key as $type => $option) {
                if ($type !== 'private') {
                    $key['private'] = $option;
                    unset ($key[$type]);
                }
            }
        } else {
            $key = array('private' => $key);
        }
        if ($passphrase !== null) {
            $this -> setPassphrase ($passphrase);
        }
        return $this -> _setKeys ($key);
    }

    /**
     * Returns all envelope keys
     * @return array
     */
    public function getEnvelopeKey () {
        $key = $this -> _keys['envelope'];
        return $key;
    }

    /**
     * Sets envelope keys
     * @param string|array $options Envelope keys
     * @return Zend_Filter_Encrypt_Openssl
     */
    public function setEnvelopeKey ($key) {
        if (is_array ($key)) {
            foreach ($key as $type => $option) {
                if ($type !== 'envelope') {
                    $key['envelope'] = $option;
                    unset ($key[$type]);
                }
            }
        } else {
            $key = array('envelope' => $key);
        }
        return $this -> _setKeys ($key);
    }

    /**
     * Returns the passphrase
     * @return string
     */
    public function getPassphrase () {
        return $this -> _passphrase;
    }

    /**
     * Sets a new passphrase
     * @param string $passphrase
     * @return Zend_Filter_Encrypt_Openssl
     */
    public function setPassphrase ($passphrase) {
        $this -> _passphrase = $passphrase;
        return $this;
    }

    /**
     * Returns the compression
     * @return array
     */
    public function getCompression () {
        return $this -> _compression;
    }

    /**
     * Sets a internal compression for values to encrypt
     * @param string|array $compression
     * @return Zend_Filter_Encrypt_Openssl
     */
    public function setCompression ($compression) {
        if (is_string ($this -> _compression)) {
            $compression = array('adapter' => $compression);
        }
        $this -> _compression = $compression;
        return $this;
    }

    /**
     * Returns if header should be packaged
     * @return boolean
     */
    public function getPackage () {
        return $this -> _package;
    }

    /**
     * Sets if the envelope keys should be included in the encrypted value
     * @param boolean $package
     * @return Zend_Filter_Encrypt_Openssl
     */
    public function setPackage ($package) {
        $this -> _package = (boolean) $package;
        return $this;
    }

    /**
     * Encrypts $value with the defined settings
     * Note that you also need the "encrypted" keys to be able to decrypt
     * @param string $value Content to encrypt
     * @return string The encrypted content
     * @throws Zend_Filter_Exception
     */
    public function encrypt ($value) {
        $encrypted = array();
        $encryptedkeys = array();
        if (count ($this -> _keys['public']) == 0) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception ('Openssl can not encrypt without public keys');
        }
        $keys = array();
        $fingerprints = array();
        $count =  - 1;
        foreach ($this -> _keys['public'] as $key => $cert) {
            $keys[$key] = openssl_pkey_get_public ($cert);
            if ($this -> _package) {
                $details = openssl_pkey_get_details ($keys[$key]);
                if ($details === false) {
                    $details = array('key' => 'ZendFramework');
                }
                 ++ $count;
                $fingerprints[$count] = md5 ($details['key']);
            }
        }
        // compress prior to encryption
        if ( ! empty ($this -> _compression)) {
            require_once 'Zend/Filter/Compress.php';
            $compress = new Zend_Filter_Compress ($this -> _compression);
            $value = $compress -> filter ($value);
        }
        $crypt = openssl_seal ($value, $encrypted, $encryptedkeys, $keys);
        foreach ($keys as $key) {
            openssl_free_key ($key);
        }
        if ($crypt === false) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception ('Openssl was not able to encrypt your content with the given options');
        }
        $this -> _keys['envelope'] = $encryptedkeys;
        // Pack data and envelope keys into single string
        if ($this -> _package) {
            $header = pack ('n', count ($this -> _keys['envelope']));
            foreach ($this -> _keys['envelope'] as $key => $envKey) {
                $header .= pack ('H32n', $fingerprints[$key], strlen ($envKey)) . $envKey;
            }
            $encrypted = $header . $encrypted;
        }
        return $encrypted;
    }

    /**
     * Defined by Zend_Filter_Interface
     * Decrypts $value with the defined settings
     * @param string $value Content to decrypt
     * @return string The decrypted content
     * @throws Zend_Filter_Exception
     */
    public function decrypt ($value) {
        $decrypted = "";
        $envelope = current ($this -> getEnvelopeKey ());
        if (count ($this -> _keys['private']) !== 1) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception ('Please give a private key for decryption with Openssl');
        }
        if ( ! $this -> _package && empty ($envelope)) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception ('Please give a envelope key for decryption with Openssl');
        }
        foreach ($this -> _keys['private'] as $key => $cert) {
            $keys = openssl_pkey_get_private ($cert, $this -> getPassphrase ());
        }
        if ($this -> _package) {
            $details = openssl_pkey_get_details ($keys);
            if ($details !== false) {
                $fingerprint = md5 ($details['key']);
            } else {
                $fingerprint = md5 ("ZendFramework");
            }
            $count = unpack ('ncount', $value);
            $count = $count['count'];
            $length = 2;
            for ($i = $count; $i > 0;  -- $i) {
                $header = unpack ('H32print/nsize', substr ($value, $length, 18));
                $length += 18;
                if ($header['print'] == $fingerprint) {
                    $envelope = substr ($value, $length, $header['size']);
                }
                $length += $header['size'];
            }
            // remainder of string is the value to decrypt
            $value = substr ($value, $length);
        }
        $crypt = openssl_open ($value, $decrypted, $envelope, $keys);
        openssl_free_key ($keys);
        if ($crypt === false) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception ('Openssl was not able to decrypt you content with the given options');
        }
        // decompress after decryption
        if ( ! empty ($this -> _compression)) {
            require_once 'Zend/Filter/Decompress.php';
            $decompress = new Zend_Filter_Decompress ($this -> _compression);
            $decrypted = $decompress -> filter ($decrypted);
        }
        return $decrypted;
    }

    /**
     * Returns the adapter name
     * @return string
     */
    public function toString () {
        return 'Openssl';
    }

}
